#ifdef _WIN32
#include "audio.h"
#include "audio_output_win32.h"

#include <windows.h>
#include <mmsystem.h>
#include <iostream>
#include <limits>

#pragma comment(lib, "winmm.lib")

using namespace krryn::audio;
using namespace krryn::audio_imp;

namespace krryn{
namespace audio_imp{
	static void CALLBACK audio_driver_win32_callback(HWAVEOUT, UINT, DWORD_PTR, DWORD, DWORD);

	class output_win32_helper{
	public:
		output_win32_helper() : m_BlockSize(8 Kb), m_BlockCount(32) {
			if(m_BlockSize & (m_BlockSize - 1)){
				throw audio::exception("Block size must be a power of two");
			}

			if(m_BlockCount & (m_BlockCount - 1)){
				throw audio::exception("Block count must be a power of two");
			}

			m_FreeBlocks = m_BlockCount;
			m_CurrentBlock = 0;

			initialize_format();
			InitializeCriticalSection(&m_CriticalSection);

			if(waveOutOpen(
				&m_Handle, 
				WAVE_MAPPER, 
				&m_Format, 
				(DWORD_PTR)audio_driver_win32_callback,
				(DWORD_PTR)this, 
				CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
			{
				throw audio::exception("Unable to open audio device");
			}

			allocate_blocks(m_BlockSize, m_BlockCount);
		}

		~output_win32_helper(){
			for(int i = 0; i < m_FreeBlocks; i++){
				if(m_Buffer[i].dwFlags & WHDR_PREPARED){
					waveOutUnprepareHeader(m_Handle, &m_Buffer[i], sizeof(WAVEHDR));
				}
			}

			free_blocks();
			waveOutClose(m_Handle);
			DeleteCriticalSection(&m_CriticalSection);
		}

		void write_audio(LPSTR a_Data, int a_Size){
			WAVEHDR *l_Current = m_Buffer + m_CurrentBlock;
			DWORD l_Remaining;

			while(a_Size > 0){
				if(l_Current->dwFlags & WHDR_PREPARED){
					waveOutUnprepareHeader(m_Handle, l_Current, sizeof(WAVEHDR));
				}

				if(a_Size < (int)(m_BlockSize - l_Current->dwUser)){
					memcpy(l_Current->lpData + l_Current->dwUser, a_Data, a_Size);
					l_Current->dwUser += a_Size;
					break;
				}

				l_Remaining = m_BlockSize - (DWORD)l_Current->dwUser;
				memcpy(l_Current->lpData + l_Current->dwUser, a_Data, l_Remaining);
				a_Size -= l_Remaining;
				a_Data += l_Remaining;
				l_Current->dwBufferLength = m_BlockSize;

				waveOutPrepareHeader(m_Handle, l_Current, sizeof(WAVEHDR));
				waveOutWrite(m_Handle, l_Current, sizeof(WAVEHDR));

				decrease_counter();

				// idle spin till more work comes up
				while(!m_FreeBlocks){
					Sleep(10);
				}

				// increment and wrap around the current block
				// block count should be a power of two, but that's
				// been taken care of in the constructor.
				m_CurrentBlock = (m_CurrentBlock + 1) & (m_BlockCount - 1);

				l_Current = m_Buffer + m_CurrentBlock;
				l_Current->dwUser = 0;
			}
		}


		void allocate_blocks(DWORD a_Size, int a_Amount){
			DWORD l_TotalSize = (a_Size + sizeof(WAVEHDR)) * a_Amount;
			LPSTR l_Buffer = (LPSTR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, l_TotalSize);

			if(l_Buffer == NULL){
				m_Buffer = 0;
				return;
			}

			m_Buffer = (WAVEHDR*)l_Buffer;
			l_Buffer += sizeof(WAVEHDR) * a_Amount;

			for(int i = 0; i < a_Amount; i++){
				m_Buffer[i].dwBufferLength = a_Size;
				m_Buffer[i].lpData = l_Buffer;
				l_Buffer += a_Size;
			}
		}

		void free_blocks(){
			HeapFree(GetProcessHeap(), 0, m_Buffer);
		}

		void decrease_counter(){
			EnterCriticalSection(&m_CriticalSection);
			m_FreeBlocks--;
			LeaveCriticalSection(&m_CriticalSection);
		}

		void increase_counter(){
			EnterCriticalSection(&m_CriticalSection);
			m_FreeBlocks++;
			LeaveCriticalSection(&m_CriticalSection);
		}

		void initialize_format(){
			m_Format.nSamplesPerSec = 44100;
			m_Format.wBitsPerSample = 16;
			m_Format.nChannels = 2;
			m_Format.cbSize = 0;
			m_Format.wFormatTag = WAVE_FORMAT_PCM;
			m_Format.nBlockAlign = (m_Format.wBitsPerSample >> 3) * m_Format.nChannels;
			m_Format.nAvgBytesPerSec = m_Format.nBlockAlign * m_Format.nSamplesPerSec;
		}

		void handle_callback(){
			increase_counter();
		}

		CRITICAL_SECTION m_CriticalSection;
		HWAVEOUT m_Handle;
		WAVEFORMATEX m_Format;
		WAVEHDR *m_Buffer;

		const int m_BlockSize;
		const int m_BlockCount;
		int m_CurrentBlock;
		int m_FreeBlocks;
	};

	static void CALLBACK audio_driver_win32_callback(HWAVEOUT, UINT a_Message, 
		DWORD_PTR a_Instance, DWORD, DWORD){

		if(a_Message == WOM_DONE){
			output_win32_helper *l_Driver = reinterpret_cast<output_win32_helper*>(a_Instance);
			l_Driver->handle_callback();
		}
	}
}
}

int output_win32::sample_rate(){
	return m_Helper->m_Format.nSamplesPerSec;
}

void output_win32::write(sample::ptr a_Sample){
	audio::int_16 *l_Dest = new audio::int_16[a_Sample->count()];

	audio::sample::value_type *l_Source = a_Sample->buffer();

	for(int i = 0; i < a_Sample->count(); i++){
		// clip the final range
		if(*l_Source > +1) *l_Source = +1;
		if(*l_Source < -1) *l_Source = -1;

		// convert the intermediate format to 16 bits for outputting
		l_Dest[i] = static_cast<audio::int_16>(
			*l_Source++ * (std::numeric_limits<audio::int_16>::max)()
		);
	}

	m_Helper->write_audio((LPSTR)l_Dest, a_Sample->count() * sizeof(audio::int_16));

	delete[] l_Dest;
}

output_win32::output_win32(){
	m_Helper = new output_win32_helper();
}

output_win32::~output_win32() {
	delete m_Helper, m_Helper = 0;
}

output::ptr output::create(){
	return output::ptr(new output_win32());
}

#endif
